Runoff - CS50x 2023
在这个程序中,你将实现一个模拟决胜选举的程序,如下所示:
./runoff Alice Bob Charlie
Number of voters: 5
Rank 1: Alice
Rank 2: Bob
Rank 3: Charlie
Rank 1: Alice
Rank 2: Charlie
Rank 3: Bob
Rank 1: Bob
Rank 2: Charlie
Rank 3: Alice
Rank 1: Bob
Rank 2: Alice
Rank 3: Charlie
Rank 1: Charlie
Rank 2: Alice
Rank 3: Bob
Alice
背景
您已经了解了简单多数选举,它遵循一个非常简单的算法来确定选举的获胜者:每个选民都获得一票,并且获得最多票数的候选人获胜。
但是,简单多数投票确实有一些缺点。例如,如果在有三名候选人的选举中,投了以下选票,会发生什么情况?
简单多数制会判决 Alice 和 Bob 在此打成平手,因为他们各自获得两票。但这真的是公平的结果吗?
还有另一种投票制度,叫做排序选择投票制。在排序选择制度中,选民可以投票给多名候选人。他们可以按照偏好顺序给候选人排序,而不仅仅是投票给第一选择。因此,生成的选票可能如下所示。
在这里,除了指定他们的首选候选人之外,每位选民还指出了他们的第二和第三选择。这样一来,原本平局的选举就可能产生赢家。最初 Alice 和 Bob 并列,所以 Charlie 被淘汰。但是,投给 Charlie 的选民,他们的第二选择是 Alice 而不是 Bob,所以 Alice 就能被判定为胜出。
排序选择投票还可以解决简单多数投票的另一个潜在缺点。看看下面的选票。
谁应该赢得这次选举?在简单多数投票中,每个选民只选择他们的首选,Charlie 以四票赢得这次选举,而 Bob 只有三票,Alice 只有两票。但是,大多数选民(9 个人里有 5 个)如果选 Alice 或者 Bob,而不是 Charlie,会更满意。通过考虑排名偏好,投票系统可以选择更能反映选民偏好的获胜者。
一种这样的排序选择投票制度就是立即决胜制。在立即决胜选举中,选民可以按照意愿给尽可能多的候选人排序。如果任何候选人获得超过半数(50%以上)的第一轮选票,该候选人就会被宣布为选举获胜者。
如果没有候选人获得超过 50% 的选票,则会发生“立即决胜”。得票最少的候选人会被淘汰出局,而最初选择该候选人为第一选择的选民,他们的第二选择将会被计入。为什么要这么做? 实际上,这模拟了如果一开始最不受欢迎的候选人就不参选的情况。
这个过程不断重复:如果没有候选人获得过半选票,那么得票最少的候选人就会被淘汰,而原本投给他们的选民,将会改投他们尚未被淘汰的下一顺位选择。一旦候选人获得多数席位,该候选人将被宣布为获胜者。
让我们以上面的九张选票为例,看看决胜选举是如何进行的。 爱丽丝得了两票,鲍勃得了三票,查理得了四票。对于一个有九个人的选举来说,需要多数票(五票)才能获胜。由于没有人获得多数票,因此需要进行第二轮投票。爱丽丝获得的票数最少(只有两票),因此爱丽丝被淘汰。原本投票给爱丽丝的选民,他们的第二选择是鲍勃,因此,鲍勃获得了这两张额外的票。鲍勃现在有五票,而查理仍然有四票。鲍勃现在获得了多数票,因此鲍勃被宣布为获胜者。
这里我们需要考虑哪些特殊情况?
一种可能是,在决定淘汰谁时出现平局。我们可以通过淘汰所有并列末位的候选人来解决这个问题。但是,如果所有剩余候选人票数相同,淘汰末位就意味着淘汰所有人!因此,在这种情况下,我们必须小心,不能淘汰所有人,只需宣布所有剩余候选人平局即可。
有些第二轮投票不需要选民对所有选项排序——例如,选举中有五位候选人,但选民可能只选两位。但是,出于本问题的目的,我们将忽略这个特殊的极端情况,并假设所有选民都将按照他们喜欢的顺序对所有候选人进行排名。
听起来是不是比简单多数投票复杂一些?但这种选举制度的优点在于,获胜者更能代表选民的意愿。
开始
登录 cs50.dev,单击您的终端窗口,然后单独执行 cd
。您应该看到类似以下的终端提示符:
接下来执行
wget https://cdn.cs50.net/2022/fall/psets/3/runoff.zip
以便将名为 runoff.zip
的 ZIP 文件下载到您的 codespace 中。
然后执行
创建一个名为 runoff
的文件夹。您不再需要 ZIP 文件,因此您可以执行
并在提示符下回复“y”,然后按 Enter 键以删除您下载的 ZIP 文件。
现在输入
然后按 Enter 键将自己移动到(即打开)该目录。您的提示符现在应类似于以下内容。
如果一切顺利,您应该执行
并看到一个名为 runoff.c
的文件。执行 code runoff.c
应该会打开该文件,您将在其中键入此问题集的代码。如果不是,请回溯您的步骤,看看您是否可以确定您在哪里出错!
理解
让我们看一下 runoff.c
。我们定义了两个常量:MAX_CANDIDATES
用于选举中候选人的最大数量,MAX_VOTERS
用于选举中选民的最大数量。
接下来是一个二维数组 preferences
。preferences[i]
数组代表选民 i
的所有偏好,preferences[i][j]
则存储选民 i
的第 j
偏好候选人的索引。
接下来是一个名为 candidate
的 struct
。每个 candidate
都有一个 string
字段用于他们的 name
,一个 int
表示他们当前拥有的 votes
数量,以及一个名为 eliminated
的 bool
值,指示该候选人是否已从选举中淘汰。数组 candidates
将跟踪选举中的所有候选人。
该程序还有两个全局变量:voter_count
和 candidate_count
。
接下来我们看看 main
函数。请注意,在确定候选人和选民人数之后,主要的投票循环就开始了,让每位选民都能参与投票。选民输入偏好后,程序会调用 vote
函数来记录这些偏好。如果任何选票被判定为无效,程序就会退出。
所有选票投完后,程序会进入另一个循环。这个循环会不断重复决胜选举的流程:检查是否有获胜者,并淘汰得票最少的候选人,直到决出胜者为止。
首先,程序会调用 tabulate
函数,该函数会根据所有选民的偏好,统计每位选民尚未被淘汰的首选候选人的得票数。接着,print_winner
函数会打印出获胜者(如果存在),程序随即结束。否则,程序会调用 find_min
函数来确定仍在选举中的候选人所得的最低票数。如果所有候选人都得票相同(由 is_tie
函数判断),则宣布平局。否则,程序会调用 eliminate
函数来淘汰得票最少的候选人。
再往下看看,你会发现这些函数——vote
、tabulate
、print_winner
、find_min
、is_tie
和 eliminate
——都需要你来实现!
规范
完成 runoff.c
的代码编写,使其能够模拟决胜选举。你应该完成 vote
、tabulate
、print_winner
、find_min
、is_tie
和 eliminate
函数的实现,并且你不应该修改 runoff.c
中的任何其他内容(以及你想要包含的额外头文件)。
vote
完成 vote
函数。
- 这个函数接受
voter
、rank
和name
这三个参数。如果name
与某个有效候选人的姓名一致,则更新全局偏好数组,记录选民voter
对该候选人的偏好等级为rank
(0
代表第一偏好,1
代表第二偏好,以此类推)。 - 如果偏好已成功记录,则该函数应返回
true
;否则,该函数应返回false
(例如,如果name
不是其中一位候选人的姓名)。 - 你可以假设没有两个候选人会同名。
提示
- 回想一下,
candidate_count
存储了选举中候选人的数量。 - 回想一下,你可以使用
strcmp
来比较两个字符串。 - 回想一下,
preferences[i][j]
存储了候选人的索引,该候选人是第i
位选民的第j
个排名偏好。
tabulate
完成 tabulate
函数。
- 这个函数需要更新每个候选人在当前阶段的得票数 (
votes
)。 - 记住,在决胜选举的每个阶段,每位选民实际上都是在给他们尚未被淘汰的首选候选人投票。
提示
- 记住,
voter_count
存储了选民人数,而且本次选举中,每位选民都有一张选票。 - 记住,对于选民
i
,他们的第一选择是preferences[i][0]
,第二选择是preferences[i][1]
,以此类推。 - 记住,
candidate
结构体中有一个名为eliminated
的字段,如果候选人被淘汰,该字段的值为true
。 - 记住,
candidate
结构体中有一个名为votes
的字段,你需要更新这个字段来记录每位候选人的得票数。 - 一旦你为选民选择了第一位未被淘汰的候选人,就应该停止,不要继续遍历他们的选择!记住,你可以使用
break
语句在满足条件时跳出循环。
print_winner
完成 print_winner
函数。
- 如果任何候选人获得超过一半的选票,则应打印他们的名字,并且该函数应返回
true
。 - 如果还没有人赢得选举,该函数应返回
false
。
提示
- 记住,
voter_count
存储了选举中选民的数量。 那么,要赢得选举需要多少票呢?
find_min
完成 find_min
函数。
- 该函数应返回仍在选举中的任何候选人的最低票数总数。
提示
- 你可能需要遍历所有候选人,找到仍在选举中且票数最少的那个。在遍历时,你需要记录哪些信息呢?
is_tie
完成 is_tie
函数。
- 该函数接受一个参数
min
,它代表当前候选人中的最低票数。 - 如果选举中剩余的每个候选人都具有相同数量的选票,则该函数应返回
true
,否则应返回false
。
提示
- 记住,如果仍在选举中的每个候选人都具有相同数量的选票,则会发生平局。 另请注意,
is_tie
函数接收一个参数min
,它代表当前候选人中的最低票数。 你该如何利用这个信息来判断是否平局呢?
eliminate
完成 eliminate
函数。
- 该函数接受一个参数
min
,它代表当前选举中任何人拥有的最低票数。 - 该函数应淘汰具有
min
票数的候选人(或候选人)。
讲解
用法
你的程序应按照以下示例运行:
./runoff Alice Bob Charlie
Number of voters: 5
Rank 1: Alice
Rank 2: Charlie
Rank 3: Bob
Rank 1: Alice
Rank 2: Charlie
Rank 3: Bob
Rank 1: Bob
Rank 2: Charlie
Rank 3: Alice
Rank 1: Bob
Rank 2: Charlie
Rank 3: Alice
Rank 1: Charlie
Rank 2: Alice
Rank 3: Bob
Alice
测试
请务必测试你的代码,确保它能处理…
- 可以有任意数量候选人的选举 (最多
MAX
,即9
位) - 根据姓名给候选人投票
- 对不在选票上的候选人进行无效投票
- 如果只有一位胜出者,则打印胜出者的姓名
- 如果所有剩余的候选人都打成平局,则不淘汰任何人
请执行以下命令,使用 check50
评估代码的正确性。但请务必自行编译并进行测试!
check50 cs50/problems/2023/x/runoff
执行以下命令,使用 style50
评估代码的风格。
如何提交
在您的终端中,执行以下命令以提交您的作品。
submit50 cs50/problems/2023/x/runoff